package com.huanletao.dom4j;

import com.huanletao.annotation.AnnotationUtils;
import com.huanletao.dom4j.annotation.ElementFormat;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import javax.annotation.Nonnull;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.Objects;

/**
 * <p>
 * xml 文档构建
 * 通过注解方式实现,文档构建过程
 *
 * </p>
 * package: com.huanletao.dom4j
 * Description:
 * className: XmlDocumentBuilder
 * author: 踏雪无痕
 * copyright: Copyright (c) 2022/7/30 14:35
 * since: 1.0.0
 */
public abstract class DocumentBuilder {

    private static final String namespace_item_separator = ";";
    private static final String namespace_prefix_open = ":";
    private static final String namespace_prefix_separator = "=";

    private static final Formatter formatter = new DocumentBuilder.DefaultXmlDocumentFormatter();

    /**
     * @param data
     * @param clazz
     * @param <T>
     * @return
     */

    public static <T> String build(T data, Class<? extends T> clazz) throws DocumentException {
        //创建文档.
        Document document = DocumentHelper.createDocument();
        document.setXMLEncoding("UTF-8");
        XmlRootElement xmlRootElement = getXmlRootElementAnnotation(clazz);

        Element root = DocumentHelper.createElement(xmlRootElement.name());
        if (StringUtils.containsNone(xmlRootElement.namespace(), "##")) {
            createRootNamespace(xmlRootElement, root);
        }
        document.setRootElement(root);
        createChildrenElement(data, clazz, root);
        return formatter.format(document);
    }


    /**
     * 在指定节点下创建文档结构,
     * 根据javapojo 类型进行创建.
     *
     * @param instance
     * @param clazz
     * @param xmlStr
     * @param xpathExpression
     * @param <T>
     * @return
     */
    public static <T> String build(T instance, Class<? extends T> clazz, String xmlStr, @Nonnull String xpathExpression) throws DocumentException {
        //创建文档.
        Document document = null;
        try {
            document = DocumentHelper.parseText(xmlStr);
        } catch (DocumentException e) {
            ExceptionUtils.rethrow(e);
        }
        Element rootElement = document.getRootElement();
        Element parentElement = (Element) rootElement.selectSingleNode(xpathExpression);
        document.setXMLEncoding("UTF-8");
        XmlRootElement xmlRootElement = getXmlRootElementAnnotation(clazz);
        String name = xmlRootElement.name();
        if (StringUtils.contains(xmlRootElement.name(), ":")) {
            name = StringUtils.substringAfter(xmlRootElement.name(), ":");
        }
        if (StringUtils.compare(name, parentElement.getName()) != 0) {
            //如果查找的节点已经等于根节点，不创建根节点，
            parentElement = parentElement.addElement(name);
        }

        if (StringUtils.containsNone(xmlRootElement.namespace(), "##")) {
            createRootNamespace(xmlRootElement, parentElement);
        }
        createChildrenElement(instance, clazz, parentElement);
        return formatter.format(document);
    }

    /**
     * 获取注解元素
     *
     * @param clazz
     * @param <T>
     * @return
     */
    private static <T> XmlRootElement getXmlRootElementAnnotation(Class<T> clazz) {
        XmlRootElement annotation = AnnotationUtils.findAnnotation(clazz, XmlRootElement.class);
        if (annotation == null) {
            annotation = AnnotationUtils.findAnnotation(clazz.getSuperclass(), XmlRootElement.class);
        }
        return annotation;
    }

    /**
     * 创建子节点.
     *
     * @param data
     * @param clazz
     * @param parentElement
     * @param <T>
     * @throws IllegalAccessException
     */
    @SuppressWarnings("all")
    private static void createChildrenElement(Object data, Class<?> clazz, Element parentElement) throws DocumentException {

        Field[] allFields = ArrayUtils.addAll(clazz.getDeclaredFields(), clazz.getSuperclass().getDeclaredFields());
        for (Field field : allFields) {
            if (!AnnotationUtils.hasAnnotation(field, XmlElement.class)) {
                continue;
            }

            Object value = null;
            try {
                PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
                Method readMethod = pd.getReadMethod();
                value = readMethod.invoke(data);
            } catch (Exception e) {
                ExceptionUtils.rethrow(e);
            }

            XmlElement annotation = AnnotationUtils.findAnnotation(field, XmlElement.class);
            Element childElement = parentElement.addElement(annotation.name());

            if (Objects.isNull(value)) {
                continue;
            }

            Type fieldGenericType = field.getGenericType();
            if (!(fieldGenericType instanceof Class<?>)) {
                Type genType = clazz.getGenericSuperclass();
                Type params = ((ParameterizedType) genType).getActualTypeArguments()[0];
                Class<?> paramsType = null;
                try {
                    paramsType = Class.forName(params.getTypeName());
                } catch (ClassNotFoundException e) {
                    ExceptionUtils.rethrow(e);
                }
                if (AnnotationUtils.hasAnnotation(paramsType, XmlRootElement.class)) {
                    createChildrenElement(value, paramsType, childElement);
                }
                continue;
            }

            if (AnnotationUtils.hasAnnotation(field.getType(), XmlRootElement.class)) {
                createChildrenElement(value, (Class<?>) field.getType(), childElement);
                continue;
            }

            if (field.getType() == Date.class) {
                ElementFormat properties = AnnotationUtils.findAnnotation(field, ElementFormat.class);
                if (properties == null) {
                    throw new DocumentException(String.format("Date.class field name is  %s requirement annotation ElementFormat to format date ", field.getName()));
                }
                value = DateFormatUtils.format((Date) value, properties.pattern());
            }

            childElement.setText(value.toString());
        }
    }

    /**
     * 创建根节点的命名空间
     *
     * @param xmlRootElement
     * @param root
     */
    private static void createRootNamespace(XmlRootElement xmlRootElement, Element root) {
        String namespace = xmlRootElement.namespace();
        String[] namespaceItems = StringUtils.split(namespace, namespace_item_separator);
        for (String namespaceItem : namespaceItems) {
            String prefix = StringUtils.substringBetween(namespaceItem, namespace_prefix_open, namespace_prefix_separator);
            String uri = StringUtils.substringAfter(namespaceItem, namespace_prefix_separator);
            root.addNamespace(prefix, uri);
        }
    }


    protected static class DefaultXmlDocumentFormatter implements Formatter {

        @Override
        public String format(Document document) {
            OutputFormat format = OutputFormat.createPrettyPrint();
            format.setNewLineAfterDeclaration(false);
            format.setEncoding(document.getXMLEncoding());
            StringWriter stringWriter = new StringWriter();
            XMLWriter writer = new XMLWriter(stringWriter, format);
            try {
                writer.write(document);
            } catch (IOException e) {
                ExceptionUtils.rethrow(e);
            } finally {
                try {
                    writer.close();
                } catch (IOException e) {
                    ExceptionUtils.rethrow(e);
                }
            }
            return stringWriter.toString();
        }
    }

}
